/*

  这是一个简单的MJPEG流媒体web服务器实现的AI-Thinker ESP32-CAM和ESP-EYE模块。
  这是测试工作与VLC和Blynk视频小部件，可以支持多达10台设备同时连接的流客户端。
  同步流是通过FreeRTOS任务实现的。

  Board: AI-Thinker ESP32-CAM or ESP-EYE
  Compile as:
   ESP32 Dev Module
   CPU Freq: 240
   Flash Freq: 80
   Flash mode: QIO
   Flash Size: 4Mb
   Patrition: Minimal SPIFFS
   PSRAM: Enabled
*/

// ESP32有两个核心:应用核心和进程核心(运行ESP32 SDK栈的核心)
#define APP_CPU 1
#define PRO_CPU 0

#include "OV2640.h"
#include <WiFi.h>
#include <WebServer.h>
#include <WiFiClient.h>

#include <esp_bt.h>
#include <esp_wifi.h>
#include <esp_sleep.h>
#include <driver/rtc_io.h>
#include <esp_http_client.h>


// Select camera model
//#define CAMERA_MODEL_WROVER_KIT
//#define CAMERA_MODEL_ESP_EYE
//#define CAMERA_MODEL_M5STACK_PSRAM
//#define CAMERA_MODEL_M5STACK_WIDE
#define CAMERA_MODEL_AI_THINKER

#include "camera_pins.h"

/*
  下一个是包含wifi凭证。
  这是你需要做的:
  1. 在Arduino IDE的“libraries”文件夹的同一文件夹或单独的子文件夹下创建一个名为“home_wifi_multi.h”的文件。(你实际上是在创建一个“假”库——我称之为“MySettings”)。
  2. 将以下文本放入文件中:
  #define SSID1 "替换为你的wifi ssid"
  #define PWD1 "替换你的wifi密码"
  3. 保存。
  应该可以工作
*/
#include "home_wifi_multi.h"

OV2640 cam;

WebServer server(80);

// ===== rtos 任务句柄 =========================
// 流是通过3个任务实现的:
TaskHandle_t tMjpeg;   // 处理客户端到web服务器的连接
TaskHandle_t tCam;     // 处理从相机获取相框并将其存储在本地
TaskHandle_t tStream;  // 实际上流式传输帧到所有连接的客户端

// frameSync信号量用于防止流缓冲，因为它被下一帧取代
SemaphoreHandle_t frameSync = NULL;

// 队列存储我们正在流式传输的当前连接的客户端
QueueHandle_t streamingClients;

// FPS刷新率设置
const int FPS = 7;

// 我们将每50毫秒(20赫兹)处理一次web客户端请求。
const int WSINTERVAL = 100;

// 设置服务器URL
const char* serverUrl = "http://服务器IP地址:端口/upload";

void takeAndSendPhoto();


// ======== 服务器连接处理程序任务 ==========================
void mjpegCB(void* pvParameters) {
  TickType_t xLastWakeTime;
  const TickType_t xFrequency = pdMS_TO_TICKS(WSINTERVAL);

  // 创建帧同步信号量并初始化它
  frameSync = xSemaphoreCreateBinary();
  xSemaphoreGive( frameSync );

  // 创建一个队列来跟踪所有连接的客户端
  streamingClients = xQueueCreate( 10, sizeof(WiFiClient*) );

  //===  安装部分  ==================

  //  创建从相机抓取帧的RTOS任务
  xTaskCreatePinnedToCore(
    camCB,        // 回调
    "cam",        // name
    4096,         // stacj大小
    NULL,         // parameters
    2,            // 优先级
    &tCam,        // RTOS任务句柄
    APP_CPU);     // 核心

  //  创建任务将流推送到所有连接的客户端
  xTaskCreatePinnedToCore(
    streamCB,
    "strmCB",
    4 * 1024,
    NULL, //(void*) handler,
    2,
    &tStream,
    APP_CPU);

  //  注册web服务器处理例程
  server.on("/mjpeg/1", HTTP_GET, handleJPGSstream);
  server.on("/jpg", HTTP_GET, handleJPG);
  server.onNotFound(handleNotFound);

  //  开启网络服务器
  server.begin();

  //=== loop() section  ===================
  xLastWakeTime = xTaskGetTickCount();
  for (;;) {
    server.handleClient();

    //  在每个服务器客户机处理请求之后，我们让其他任务运行，然后暂停
    taskYIELD();
    vTaskDelayUntil(&xLastWakeTime, xFrequency);
  }
}


// 常用变量:
volatile size_t camSize;    // 当前帧的大小(字节)
volatile char* camBuf;      // 指向当前帧的指针


// ==== RTOS任务从相机中抓取帧 =========================
void camCB(void* pvParameters) {

  TickType_t xLastWakeTime;

  //  与当前所需帧速率相关联的运行间隔
  const TickType_t xFrequency = pdMS_TO_TICKS(1000 / FPS);

  //  互斥锁用于交换活动帧的关键部分
  portMUX_TYPE xSemaphore = portMUX_INITIALIZER_UNLOCKED;

  //  指针指向两个帧，它们各自的大小和当前帧的索引
  char* fbs[2] = { NULL, NULL };
  size_t fSize[2] = { 0, 0 };
  int ifb = 0;

  //=== loop() section  ===================
  xLastWakeTime = xTaskGetTickCount();

  for (;;) {

    //  从相机中抓取一个帧并查询它的大小
    cam.run();
    size_t s = cam.getSize();

    //  如果帧大小大于我们先前分配的帧空间-请求当前帧空间的125%
    if (s > fSize[ifb]) {
      fSize[ifb] = s * 4 / 3;
      fbs[ifb] = allocateMemory(fbs[ifb], fSize[ifb]);
    }

    //  将当前帧复制到本地缓冲区
    char* b = (char*) cam.getfb();
    memcpy(fbs[ifb], b, s);

    //  让其他任务运行并等待，直到当前帧率间隔结束(如果还有时间的话)。
    taskYIELD();
    vTaskDelayUntil(&xLastWakeTime, xFrequency);

    //  只有在当前没有帧流到客户端时才切换帧
    //  等待一个信号量，直到客户端操作完成
    xSemaphoreTake( frameSync, portMAX_DELAY );

    //  切换当前帧时不允许中断
    portENTER_CRITICAL(&xSemaphore);
    camBuf = fbs[ifb];
    camSize = s;
    ifb++;
    ifb &= 1;  //  这应该产生1,0,1,0,1…序列
    portEXIT_CRITICAL(&xSemaphore);

    //  让任何等待框架的人知道框架已经准备好了
    xSemaphoreGive( frameSync );

    //  技术上只需要一次:让流任务知道我们至少有一个帧，它可以开始向客户端发送帧(如果有的话)
    xTaskNotifyGive( tStream );

    //  立即让其他(流)任务运行
    taskYIELD();

    if ( eTaskGetState( tStream ) == eSuspended ) {
      vTaskSuspend(NULL);  // passing NULL means "suspend yourself"
    }
  }
}


// ==== 如果存在的话，利用PSRAM的内存分配器 =======================
char* allocateMemory(char* aPtr, size_t aSize) {

  //  由于当前缓冲区太小，释放它
  if (aPtr != NULL) free(aPtr);


  size_t freeHeap = ESP.getFreeHeap();
  char* ptr = NULL;

  // 如果请求的内存超过当前空闲堆的2/3，请立即尝试使用PSRAM
  if ( aSize > freeHeap * 2 / 3 ) {
    if ( psramFound() && ESP.getFreePsram() > aSize ) {
      ptr = (char*) ps_malloc(aSize);
    }
  }
  else {
    //  足够的空闲堆-让我们尝试分配快速RAM作为缓冲区
    ptr = (char*) malloc(aSize);

    //  如果堆上的分配失败，让我们再给PSRAM一次机会:
    if ( ptr == NULL && psramFound() && ESP.getFreePsram() > aSize) {
      ptr = (char*) ps_malloc(aSize);
    }
  }

  // 最后，如果内存指针为NULL，则无法分配任何内存，这是一个终止条件。
  if (ptr == NULL) {
    ESP.restart();
  }
  return ptr;
}


// ==== 流媒体 ======================================================
const char HEADER[] = "HTTP/1.1 200 OK\r\n" \
                      "Access-Control-Allow-Origin: *\r\n" \
                      "Content-Type: multipart/x-mixed-replace; boundary=123456789000000000000987654321\r\n";
const char BOUNDARY[] = "\r\n--123456789000000000000987654321\r\n";
const char CTNTTYPE[] = "Content-Type: image/jpeg\r\nContent-Length: ";
const int hdrLen = strlen(HEADER);
const int bdrLen = strlen(BOUNDARY);
const int cntLen = strlen(CTNTTYPE);


// ==== 处理来自客户端的连接请求 ===============================
void handleJPGSstream(void)
{
  //  只能容纳10个客户端。这个限制是WiFi连接的默认设置
  if ( !uxQueueSpacesAvailable(streamingClients) ) return;


  //  创建一个新的WiFi客户端对象来跟踪这个对象
  WiFiClient* client = new WiFiClient();
  *client = server.client();

  //  立即向这个客户端发送一个报头
  client->write(HEADER, hdrLen);
  client->write(BOUNDARY, bdrLen);

  // 将客户端推送到流队列
  xQueueSend(streamingClients, (void *) &client, 0);

  // 唤醒流任务，如果它们之前被挂起:
  if ( eTaskGetState( tCam ) == eSuspended ) vTaskResume( tCam );
  if ( eTaskGetState( tStream ) == eSuspended ) vTaskResume( tStream );
}


// ==== 实际上流内容到所有连接的客户端 ========================
void streamCB(void * pvParameters) {
  char buf[16];
  TickType_t xLastWakeTime;
  TickType_t xFrequency;

  //  等待，直到第一帧被捕获，有东西要发送给客户端
  ulTaskNotifyTake( pdTRUE,          /* 退出前清除通知值。 */
                    portMAX_DELAY ); 

  xLastWakeTime = xTaskGetTickCount();
  for (;;) {
    //  默认假设我们是根据FPS运行的
    xFrequency = pdMS_TO_TICKS(1000 / FPS);

    //  只有在有人看的时候才会发
    UBaseType_t activeClients = uxQueueMessagesWaiting(streamingClients);
    if ( activeClients ) {
      //  根据连接的客户端数量调整周期
      xFrequency /= activeClients;

      //  因为我们向每个人发送相同的帧，所以从队列的前面弹出一个客户端
      WiFiClient *client;
      xQueueReceive (streamingClients, (void*) &client, 0);

      //  检查该客户端是否仍然连接。

      if (!client->connected()) {
        //  如果他/她已断开连接，则删除此客户端引用，并且不再将其放回队列。再见!
        delete client;
      }
      else {

        //  好的。这是一个主动连接的客户端。
        //  让我们抓取一个信号量来防止帧在服务这个帧时发生变化
        xSemaphoreTake( frameSync, portMAX_DELAY );

        client->write(CTNTTYPE, cntLen);
        sprintf(buf, "%d\r\n\r\n", camSize);
        client->write(buf, strlen(buf));
        client->write((char*) camBuf, (size_t)camSize);
        client->write(BOUNDARY, bdrLen);

        // 由于该客户端仍处于连接状态，因此将其推到队列的末尾以进行进一步处理
        xQueueSend(streamingClients, (void *) &client, 0);

        //  框架已经准备好了。释放信号量并让其他任务运行。
        //  如果有一个帧切换准备好了，它将在帧之间发生
        xSemaphoreGive( frameSync );
        taskYIELD();
      }
    }
    else {
      //  由于没有连接的客户端，因此没有理由浪费电池运行
      vTaskSuspend(NULL);
    }
    //  让其他任务在服务每个客户端之后运行
    taskYIELD();
    vTaskDelayUntil(&xLastWakeTime, xFrequency);
  }
}



const char JHEADER[] = "HTTP/1.1 200 OK\r\n" \
                       "Content-disposition: inline; filename=capture.jpg\r\n" \
                       "Content-type: image/jpeg\r\n\r\n";
const int jhdLen = strlen(JHEADER);

// ==== 提供一个JPEG帧 =============================================
void handleJPG(void)
{
  WiFiClient client = server.client();

  if (!client.connected()) return;
  cam.run();
  client.write(JHEADER, jhdLen);
  client.write((char*)cam.getfb(), cam.getSize());
}


// ==== 处理无效的URL请求 ============================================
void handleNotFound()
{
  String message = "Server is running!\n\n";
  message += "URI: ";
  message += server.uri();
  message += "\nMethod: ";
  message += (server.method() == HTTP_GET) ? "GET" : "POST";
  message += "\nArguments: ";
  message += server.args();
  message += "\n";
  server.send(200, "text / plain", message);
}



// ==== 设置方法 ==================================================================
void setup()
{

  // 串口连接:
  pinMode(16, INPUT_PULLUP);
  Serial.begin(115200);
  Serial.setDebugOutput(true);
  Serial.println();
  delay(1000); // 等待一秒钟，让Serial连接


  // 配置摄像机
  camera_config_t config;
  config.ledc_channel = LEDC_CHANNEL_0;
  config.ledc_timer = LEDC_TIMER_0;
  config.pin_d0 = Y2_GPIO_NUM;
  config.pin_d1 = Y3_GPIO_NUM;
  config.pin_d2 = Y4_GPIO_NUM;
  config.pin_d3 = Y5_GPIO_NUM;
  config.pin_d4 = Y6_GPIO_NUM;
  config.pin_d5 = Y7_GPIO_NUM;
  config.pin_d6 = Y8_GPIO_NUM;
  config.pin_d7 = Y9_GPIO_NUM;
  config.pin_xclk = XCLK_GPIO_NUM;
  config.pin_pclk = PCLK_GPIO_NUM;
  config.pin_vsync = VSYNC_GPIO_NUM;
  config.pin_href = HREF_GPIO_NUM;
  config.pin_sscb_sda = SIOD_GPIO_NUM;
  config.pin_sscb_scl = SIOC_GPIO_NUM;
  config.pin_pwdn = PWDN_GPIO_NUM;
  config.pin_reset = RESET_GPIO_NUM;
  config.xclk_freq_hz = 20000000;
  config.pixel_format = PIXFORMAT_JPEG;

  //  帧参数:选择一个
  //  config.frame_size = FRAMESIZE_UXGA;
  //  config.frame_size = FRAMESIZE_SVGA;
  //  config.frame_size = FRAMESIZE_QVGA;
  config.frame_size = FRAMESIZE_VGA;
  config.jpeg_quality = 12;
  config.fb_count = 2;

#if defined(CAMERA_MODEL_ESP_EYE)
  pinMode(13, INPUT_PULLUP);
  pinMode(14, INPUT_PULLUP);
#endif

  if (cam.init(config) != ESP_OK) {
    Serial.println("Error initializing the camera");
    delay(10000);
    ESP.restart();
  }


  //  配置并连接WiFi
  IPAddress ip;

  WiFi.mode(WIFI_STA);
  WiFi.begin("WiFi的SSID", "密码");
  Serial.print("Connecting to WiFi");
  while (WiFi.status() != WL_CONNECTED)
  {
    delay(500);
    Serial.print(F("."));
  }
  ip = WiFi.localIP();
  Serial.println(F("WiFi connected"));
  Serial.println("");
  Serial.print("Stream Link: http://");
  Serial.print(ip);
  Serial.println("/mjpeg/1");


  // 开始主流化RTOS任务
  xTaskCreatePinnedToCore(
    mjpegCB,
    "mjpeg",
    4 * 1024,
    NULL,
    2,
    &tMjpeg,
    APP_CPU);
}


void loop() {
  vTaskDelay(1000);
  if (digitalRead(16) == LOW) {
    delay(50); // 简单防抖
    if (digitalRead(16) == LOW) {
      takeAndSendPhoto();
    }
  }
  delay(10);
}
void takeAndSendPhoto() {
  Serial.println("Taking picture...");
  
  camera_fb_t * fb = esp_camera_fb_get();
  if (!fb) {
    Serial.println("Camera capture failed");
    return;
  }
  
  Serial.println("Picture taken!");
  // 打印照片大小和部分数据
  Serial.printf("Picture size: %u bytes\n", fb->len);
  Serial.print("Picture data (first 20 bytes): ");
  for (int i = 0; i < 20; i++) {
    Serial.printf("%02X ", fb->buf[i]);
  }
  Serial.println();

  // 上传图片
  esp_http_client_config_t config = {};
  config.url = serverUrl;
  config.method = HTTP_METHOD_POST;
  config.timeout_ms = 15000; // 增加超时时间
  
  esp_http_client_handle_t client = esp_http_client_init(&config);
  esp_http_client_set_header(client, "Content-Type", "image/jpeg");
  esp_http_client_set_post_field(client, (const char *)fb->buf, fb->len);
  esp_err_t err = esp_http_client_perform(client);

  if (err == ESP_OK) {
    Serial.println("File uploaded successfully");
  } else {
    Serial.printf("File upload failed: %s\n", esp_err_to_name(err));

    // 获取详细的错误信息
    int http_status = esp_http_client_get_status_code(client);
    Serial.printf("HTTP status: %d\n", http_status);
  }

  esp_http_client_cleanup(client);
  esp_camera_fb_return(fb);
}
